/*
 *  UVCCamera
 *  library and sample to access to UVC web camera on non-rooted Android device
 *
 * Copyright (c) 2014-2017 saki t_saki@serenegiant.com
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 *
 *  All files in the folder are under this Apache License, Version 2.0.
 *  Files in the libjpeg-turbo, libusb, libuvc, rapidjson folder
 *  may have a different license, see the respective files.
 */

package com.serenegiant.service;

import android.content.Intent;
import android.graphics.Bitmap;
import android.hardware.usb.UsbDevice;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Base64;
import android.util.Log;
import android.util.SparseArray;
import android.view.Surface;

import com.serenegiant.common.BaseService;
import com.serenegiant.usb.IFrameCallback;
import com.serenegiant.usb.USBMonitor;
import com.serenegiant.usb.USBMonitor.OnDeviceConnectListener;
import com.serenegiant.usb.USBMonitor.UsbControlBlock;
import com.serenegiant.usbcameratest4.BuildConfig;
import com.serenegiant.usbcameratest4.Constants;
import com.serenegiant.usbcameratest4.MyApplication;
import com.serenegiant.usbcameratest4.R;
import com.serenegiant.widget.UVCCameraTextureView;
import com.viwo.android.FaceDetect;

import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class UVCService extends BaseService implements IFrameCallback {
	private static final boolean DEBUG = BuildConfig.LOG_DEBUG;
	private static final String TAG = "UVCService";

	private static final int NOTIFICATION = R.string.app_name;

	private USBMonitor mUSBMonitor;
	private GPIO gpio;
	private ExecutorService mySingleThreadExecutor = Executors.newSingleThreadExecutor();

	public UVCService() {
		if (DEBUG) Log.d(TAG, "Constructor:");
	}

	@Override
	public void onCreate() {
		super.onCreate();
		if (DEBUG) Log.d(TAG, "onCreate:");
		if (mUSBMonitor == null) {
			mUSBMonitor = new USBMonitor(getApplicationContext(), mOnDeviceConnectListener);
			mUSBMonitor.register();
		}
//		initGPIO();
		FaceDetect.getFaceUtil(16, 1, UVCService.this );
		gpio = new GPIO( MyApplication.getInstance().getContext() );
		gpio.init();
//		showNotification(getString(R.string.app_name));
		//mySingleThreadExecutor.execute( syncRunnable );

	}

//	Runnable syncRunnable = new Runnable() {
//		@Override
//		public void run() {
//			Log.e(TAG, Thread.currentThread().getName());
//			while( true )
//			{
//				long currTs = System.currentTimeMillis() - frameTs;
//				if( currTs > 5000 )
//				{
//					if (DEBUG) Log.d(TAG, "syncRunnable to stopSelf" );
//				}
//				if (DEBUG) Log.d(TAG, "syncRunnable :" + currTs );
//				try {
//					Thread.sleep(1000);
//				} catch (InterruptedException e) {
//					e.printStackTrace();
//				}
//			}
//		}
//	};

	@Override
	public void onDestroy() {
		if (DEBUG) Log.d(TAG, "onDestroy:");
		if (mUSBMonitor != null) {
			mUSBMonitor.unregister();
			mUSBMonitor = null;
		}
		//关闭线程池
		mySingleThreadExecutor.shutdown();
		super.onDestroy();
	}

	@Override
	public IBinder onBind(final Intent intent) {
		if (DEBUG) Log.d(TAG, "onBind:" + intent);
		final String action = intent != null ? intent.getAction() : null;
		if (IUVCService.class.getName().equals(action)) {
			Log.i(TAG, "return mBasicBinder");
			return mBasicBinder;
		}
		if (IUVCSlaveService.class.getName().equals(action)) {
			Log.i(TAG, "return mSlaveBinder");
			return mSlaveBinder;
		}
		return null;
	}

	@Override
	public void onRebind(final Intent intent) {
		if (DEBUG) Log.d(TAG, "onRebind:" + intent);
	}

	@Override
	public boolean onUnbind(final Intent intent) {
		if (DEBUG) Log.d(TAG, "onUnbind:" + intent);
		if (checkReleaseService()) {
			stopSelf();
		}
		if (DEBUG) Log.d(TAG, "onUnbind:finished");
		return true;
	}

//********************************************************************************

	private final OnDeviceConnectListener mOnDeviceConnectListener = new OnDeviceConnectListener() {
		@Override
		public void onAttach(final UsbDevice device) {
			if (DEBUG) Log.d(TAG, "OnDeviceConnectListener#onAttach:");
		}

		@Override
		public void onConnect(final UsbDevice device, final UsbControlBlock ctrlBlock, final boolean createNew) {
			if (DEBUG) Log.d(TAG, "OnDeviceConnectListener#onConnect:");

			queueEvent(new Runnable() {
				@Override
				public void run() {
					final int key = device.hashCode();
					CameraServer service;
					synchronized (sServiceSync) {
						service = sCameraServers.get(key);
						if (service == null) {
							service = CameraServer.createServer(UVCService.this, UVCService.this , ctrlBlock, device.getVendorId(), device.getProductId());
							sCameraServers.append(key, service);
						} else {
							Log.w(TAG, "service already exist before connection");
						}
						sServiceSync.notifyAll();
					}
				}
			}, 0);
		}

		@Override
		public void onDisconnect(final UsbDevice device, final UsbControlBlock ctrlBlock) {
			if (DEBUG) Log.d(TAG, "OnDeviceConnectListener#onDisconnect:");
			queueEvent(new Runnable() {
				@Override
				public void run() {
					removeService(device);
				}
			}, 0);
		}

		@Override
		public void onDettach(final UsbDevice device) {
			if (DEBUG) Log.d(TAG, "OnDeviceConnectListener#onDettach:");
		}

		@Override
		public void onCancel(final UsbDevice device) {
			if (DEBUG) Log.d(TAG, "OnDeviceConnectListener#onCancel:");
			synchronized (sServiceSync) {
				sServiceSync.notifyAll();
			}
		}
	};

	private void removeService(final UsbDevice device) {
		final int key = device.hashCode();
		synchronized (sServiceSync) {
			final CameraServer service = sCameraServers.get(key);
			if (service != null)
				service.release();
			sCameraServers.remove(key);
			sServiceSync.notifyAll();
		}
		if (checkReleaseService()) {
			stopSelf();
		}
	}
	//********************************************************************************
	private static final Object sServiceSync = new Object();
	private static final SparseArray<CameraServer> sCameraServers = new SparseArray<CameraServer>();

	/**
	 * get CameraService that has specific ID<br>
	 * if zero is provided as ID, just return top of CameraServer instance(non-blocking method) if exists or null.<br>
	 * if non-zero ID is provided, return specific CameraService if exist. block if not exists.<br>
	 * return null if not exist matched specific ID<br>
	 * @param serviceId
	 * @return
	 */
	private static CameraServer getCameraServer(final int serviceId) {
		synchronized (sServiceSync) {
			CameraServer server = null;
			if ((serviceId == 0) && (sCameraServers.size() > 0)) {
				server = sCameraServers.valueAt(0);
			} else {
				server = sCameraServers.get(serviceId);
				if (server == null)
					try {
						Log.i(TAG, "waiting for service is ready");
						sServiceSync.wait();
					} catch (final InterruptedException e) {
					}
				server = sCameraServers.get(serviceId);
			}
			return server;
		}
	}

	/**
	 * @return true if there are no camera connection
	 */
	private static boolean checkReleaseService() {
		CameraServer server = null;
		synchronized (sServiceSync) {
			final int n = sCameraServers.size();
			if (DEBUG) Log.d(TAG, "checkReleaseService:number of service=" + n);
			for (int i = 0; i < n; i++) {
				server = sCameraServers.valueAt(i);
				Log.i(TAG, "checkReleaseService:server=" + server + ",isConnected=" + (server != null && server.isConnected()));
				if (server != null && !server.isConnected()) {
					sCameraServers.removeAt(i);
					server.release();
				}
			}
			return sCameraServers.size() == 0;
		}
	}

	//********************************************************************************
	private final IUVCService.Stub mBasicBinder = new IUVCService.Stub() {
		private IUVCServiceCallback mCallback;

		@Override
		public int select(final UsbDevice device, final IUVCServiceCallback callback) throws RemoteException {
			if (DEBUG) Log.d(TAG, "mBasicBinder#select:device=" + (device !=null ? device.getDeviceName() : null));
			mCallback = callback;
			final int serviceId = device.hashCode();
			CameraServer server = null;
			synchronized (sServiceSync) {
				server = sCameraServers.get(serviceId);
				if (server == null) {
					Log.i(TAG, "request permission");
					mUSBMonitor.requestPermission(device);
					Log.i(TAG, "wait for getting permission");
					try {
						sServiceSync.wait();
					} catch (final Exception e) {
						Log.e(TAG, "connect:", e);
					}
					Log.i(TAG, "check service again");
					server = sCameraServers.get(serviceId);
					if (server == null) {
						throw new RuntimeException("failed to open USB device(has no permission)");
					}
				}
			}
			if (server != null) {
				Log.i(TAG, "success to get service:serviceId=" + serviceId);
				server.registerCallback(callback);
			}
			return serviceId;
		}

		@Override
		public void release(final int serviceId) throws RemoteException {
			if (DEBUG) Log.d(TAG, "mBasicBinder#release:");
			synchronized (sServiceSync) {
				final CameraServer server = sCameraServers.get(serviceId);
				if (server != null) {
					if (server.unregisterCallback(mCallback)) {
						if (!server.isConnected()) {
							sCameraServers.remove(serviceId);
							if (server != null) {
								server.release();
							}
							final CameraServer srv = sCameraServers.get(serviceId);
							Log.w(TAG, "srv=" + srv);
						}
					}
				}
			}
			mCallback = null;
		}

		@Override
		public boolean isSelected(final int serviceId) throws RemoteException {
			return getCameraServer(serviceId) != null;
		}

		@Override
		public void releaseAll() throws RemoteException {
			if (DEBUG) Log.d(TAG, "mBasicBinder#releaseAll:");
			CameraServer server;
			synchronized (sServiceSync) {
				final int n = sCameraServers.size();
				for (int i = 0; i < n; i++) {
					server = sCameraServers.valueAt(i);
					sCameraServers.removeAt(i);
					if (server != null) {
						server.release();
					}
				}
			}
		}

		@Override
		public void resize(final int serviceId, final int width, final int height) {
			if (DEBUG) Log.d(TAG, "mBasicBinder#resize:");
			final CameraServer server = getCameraServer(serviceId);
			if (server == null) {
				throw new IllegalArgumentException("invalid serviceId");
			}
			server.resize(width, height);
		}

		@Override
		public void connect(final int serviceId) throws RemoteException {
			if (DEBUG) Log.d(TAG, "mBasicBinder#connect:");
			final CameraServer server = getCameraServer(serviceId);
			if (server == null) {
				throw new IllegalArgumentException("invalid serviceId");
			}
			server.connect();
		}

		@Override
		public void disconnect(final int serviceId) throws RemoteException {
			if (DEBUG) Log.d(TAG, "mBasicBinder#disconnect:");
			final CameraServer server = getCameraServer(serviceId);
			if (server == null) {
				throw new IllegalArgumentException("invalid serviceId");
			}
			server.disconnect();
		}

		@Override
		public boolean isConnected(final int serviceId) throws RemoteException {
			final CameraServer server = getCameraServer(serviceId);
			return (server != null) && server.isConnected();
		}

		@Override
		public void addSurface(final int serviceId, final int id_surface, final Surface surface, final boolean isRecordable) throws RemoteException {
			if (DEBUG) Log.d(TAG, "mBasicBinder#addSurface:id=" + id_surface + ",surface=" + surface);
			final CameraServer server = getCameraServer(serviceId);
			if (server != null)
				server.addSurface(id_surface, surface, isRecordable, null);
		}

		@Override
		public void removeSurface(final int serviceId, final int id_surface) throws RemoteException {
			if (DEBUG) Log.d(TAG, "mBasicBinder#removeSurface:id=" + id_surface);
			final CameraServer server = getCameraServer(serviceId);
			if (server != null)
				server.removeSurface(id_surface);
		}

		@Override
		public boolean isRecording(final int serviceId) throws RemoteException {
			final CameraServer server = getCameraServer(serviceId);
			return server != null && server.isRecording();
		}

		@Override
		public void startRecording(final int serviceId) throws RemoteException {
			if (DEBUG) Log.d(TAG, "mBasicBinder#startRecording:");
			final CameraServer server = getCameraServer(serviceId);
			if ((server != null) && !server.isRecording()) {
				server.startRecording();
			}
		}

		@Override
		public void stopRecording(final int serviceId) throws RemoteException {
			if (DEBUG) Log.d(TAG, "mBasicBinder#stopRecording:");
			final CameraServer server = getCameraServer(serviceId);
			if ((server != null) && server.isRecording()) {
				server.stopRecording();
			}
		}

		@Override
		public void captureStillImage(final int serviceId, final String path) throws RemoteException {
			if (DEBUG) Log.d(TAG, "mBasicBinder#captureStillImage:" + path);
			final CameraServer server = getCameraServer(serviceId);
			if (server != null) {
				server.captureStill(path);
			}
		}

		@Override
		public String getFaces()
		{
			String rtn = null;
			if( ! UVCService.faceQueue.isEmpty() )
			{
				rtn =  UVCService.faceQueue.poll();
			}
			return rtn ;
		}

		@Override
		public String getQueueSize()
		{
			int size = UVCService.faceQueue.size() ;
			long ts = lastDetectTs;
			return "" + size + "," + lastDetectTs;
		}

		@Override
		public void initParams( double p1,int p4,int p5 )
		{
			face_confidence = p1;
			detect_frames_count = p4;
			detect_face_size = p5;
		}

	};

	//********************************************************************************
	private final IUVCSlaveService.Stub mSlaveBinder = new IUVCSlaveService.Stub() {
		@Override
		public boolean isSelected(final int serviceID) throws RemoteException {
			return getCameraServer(serviceID) != null;
		}

		@Override
		public boolean isConnected(final int serviceID) throws RemoteException {
			final CameraServer server = getCameraServer(serviceID);
			return server != null && server.isConnected();
		}

		@Override
		public void addSurface(final int serviceID, final int id_surface, final Surface surface, final boolean isRecordable, final IUVCServiceOnFrameAvailable callback) throws RemoteException {
			if (DEBUG) Log.d(TAG, "mSlaveBinder#addSurface:id=" + id_surface + ",surface=" + surface);
			final CameraServer server = getCameraServer(serviceID);
			if (server != null) {
				server.addSurface(id_surface, surface, isRecordable, callback);
			} else {
				Log.e(TAG, "failed to get CameraServer:serviceID=" + serviceID);
			}
		}

		@Override
		public void removeSurface(final int serviceID, final int id_surface) throws RemoteException {
			if (DEBUG) Log.d(TAG, "mSlaveBinder#removeSurface:id=" + id_surface);
			final CameraServer server = getCameraServer(serviceID);
			if (server != null) {
				server.removeSurface(id_surface);
			} else {
				Log.e(TAG, "failed to get CameraServer:serviceID=" + serviceID);
			}
		}
	};



	@Override
	public void onFrame(ByteBuffer frame) {

		frameTs = System.currentTimeMillis();
		if( detect_frames_count > 0 ) {
			if (idx++ % detect_frames_count != 0) {
				return;
			}
			idx = 1;
		}
		ArrayBlockingQueue<String> faceQueue = UVCService.faceQueue;
		if( faceQueue.size() == Constants.QUEUE_SIZE )
		{
			faceQueue.remove();
		}

		if( detect_face_size == 0 )
			return;

//		614400	640/480		2
//		4147200	1920/1080	2
		frame.get(rgba);

		String str = buildCropFaceString();

		if( str.length() > 0 )
			faceQueue.add( str );

	}

	public Bitmap getPicFromBytes(byte[] bytes,int w,int h) {
		ByteBuffer bufferRGB = ByteBuffer.wrap(bytes);//将 byte 数组包装到Buffer缓冲区中
		Bitmap videoBit = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);//创建一个指定大小格式的Bitmap对象
		try {
			bufferRGB.rewind();
			videoBit.copyPixelsFromBuffer(bufferRGB);
		} catch (Exception e){
			bufferRGB = null;
		}
		return videoBit;
	}

	public byte[] getBitmapBytes(Bitmap bitmap){
		int bytes = bitmap.getByteCount();  //返回可用于储存此位图像素的最小字节数
		ByteBuffer buffer = ByteBuffer.allocate(bytes); //  使用allocate()静态方法创建字节缓冲区
		bitmap.copyPixelsToBuffer(buffer); // 将位图的像素复制到指定的缓冲区
		byte[] rgba = buffer.array();
		return rgba;
	}

	//常用变量
	private static double face_confidence = 0.99;	//0.999
	//private static double distinct_face_confidence = 0.55;	//0.65
	//private static double blur_face_confidence = 40;	//40
	private static int detect_frames_count = 20;
	private static int detect_face_size = 25;

	public static ArrayBlockingQueue<String> faceQueue = new ArrayBlockingQueue<String>(Constants.QUEUE_SIZE );
	private byte[] rgba = new byte[ Constants.BUFFER_SIZE ];
	private int detect_left =  Constants.LEFT;
	private int detect_top = Constants.TOP;
	private byte[] crop_rgba = new byte[ Constants.DETECT_BUFFER_SIZE ];
	private int idx = 1;
	private StringBuilder builder = new StringBuilder();
	private String builderLog = "";
	private long frameTs ;
	private long lastDetectTs ;

	private String buildCropFaceString()
	{
		long t1 = System.currentTimeMillis();

		builder.delete(0,builder.length());
		Bitmap bitmap = getPicFromBytes(rgba,Constants.WIDTH, Constants.HEIGHT);
		Bitmap cropBitmap = Bitmap.createBitmap(bitmap,detect_left ,detect_top, Constants.DETECT_WIDTH , Constants.DETECT_HEIGHT );
		crop_rgba = getBitmapBytes(cropBitmap);

		long t01 = System.currentTimeMillis();
		float[] faceInfo = FaceDetect.faceUtil.FaceDetectF(crop_rgba, Constants.DETECT_WIDTH, Constants.DETECT_HEIGHT , Constants.IMAGE_CHANNEL, detect_face_size );
		long ts1 = System.currentTimeMillis() - t01;

		int faceNum = 0;
		String result_code = "0";
		int pTime = 0;
		if (faceInfo != null && faceInfo.length > 1){
			faceNum = (int) faceInfo[0];
			int ii = 0;
			for(int i=0;i<faceNum;i++) {
				float _left, _top, _right, _bottom;
				float left, top, right, bottom, width, height;
				float credible;
				_left = faceInfo[1 + 15 * i] ;
				_top = faceInfo[2 + 15 * i] ;
				_right = faceInfo[3 + 15 * i] ;
				_bottom = faceInfo[4 + 15 * i] ;
				credible = faceInfo[15 + 15 * i];

                int[] facePoints = new int[10];
				facePoints[0] = (int) (faceInfo[5 + 15 * i] - _left);
				facePoints[1] = (int) (faceInfo[6 + 15 * i] - _left);
				facePoints[2] = (int) (faceInfo[7 + 15 * i] - _left);
				facePoints[3] = (int) (faceInfo[8 + 15 * i] - _left);
				facePoints[4] = (int) (faceInfo[9 + 15 * i] - _left);
				facePoints[5] = (int) (faceInfo[10 + 15 * i] - _top);
				facePoints[6] = (int) (faceInfo[11 + 15 * i] - _top);
				facePoints[7] = (int) (faceInfo[12 + 15 * i] - _top);
				facePoints[8] = (int) (faceInfo[13 + 15 * i] - _top);
				facePoints[9] = (int) (faceInfo[14 + 15 * i] - _top);

				left = _left + detect_left;
				top = _top + detect_top;
				right = _right + detect_left;
				bottom = _bottom + detect_top;

				width = right - left;
				height = bottom - top;
				ii++;

				Bitmap nBitmap = Bitmap.createBitmap(bitmap,(int)left,(int)top,(int)width,(int)height);
				byte[] rgbai = getBitmapBytes(nBitmap);
				if( credible <= face_confidence ) {
					result_code += "c";        //置信度过滤
					continue;
				}

				String base64 = new String( Base64.encode( rgbai , Base64.DEFAULT ));

				if( builder.length() > 0 )
					builder.append("#");

				builder.append( _left + "," );
				builder.append( _top + "," );
				builder.append( _right + "," );
				builder.append( _bottom + "," );
				builder.append( Constants.DETECT_WIDTH + "," );
				builder.append( Constants.DETECT_HEIGHT + "," );
				builder.append( left + "," );
				builder.append( top  + "," );
				builder.append( right + "," );
				builder.append( bottom + "," );
//				builder.append( width + "," );
//				builder.append( height + "," );
				builder.append( credible + "," );
				//builder.append( 0 + "," );
				//builder.append( 0.0 + "," );
				builder.append( t1 + "," );

				builderLog = builder.toString();
				String featherStr = Arrays.toString( facePoints );
				featherStr = featherStr.substring(1,featherStr.length()-1).replaceAll("," ,";")
						.replaceAll(" ", "");
				builder.append( featherStr + "," );

				builder.append( base64 + "," );

				long t2 = System.currentTimeMillis() - t1;
				pTime += t2;

				//if (DEBUG) Log.i(TAG, "builder=[" + builder.toString() + "]"  );

				nBitmap.recycle();
			}
			bitmap.recycle();
			cropBitmap.recycle();
		}
		long t2 = System.currentTimeMillis() - t1;

		if (DEBUG) Log.i(TAG, "totalT=[" + t2 + "]"  + ",detectT=[" + ( pTime / faceNum ) + "]"  + ",faceNum=[" + faceNum + "]" + ",str=[" + builderLog + "]" + ",size=[" + builder.length() + "]"  );
		lastDetectTs = System.currentTimeMillis();
		return builder.toString();
	}

}

